/* -*- tab-width: 4; c-basic-offset: 2; indent-tabs-mode: nil -*- */

// Copyright (C) 2005 Sean M. Foy
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA

using System;
using System.Collections;
using System.ComponentModel;
using System.Reflection;
using System.Text;
using NUnit.Framework;
using FPUtils = seanfoy.DefensiveDatasource.Chameleon.FPUtils;

namespace seanfoy {
  /// <summary>
  /// Unit tests for the <see cref="DefensiveDatasource" />
  /// and related classes
  /// </summary>
  [TestFixture]
  public class TestDefensiveDatasource {
    protected object[] fragileList = new object[] {
      "too many secrets",
      new Queue(),
      1};

    /// <summary>
    /// Apply NUnit assertions to determine whether
    /// two objects have the same properties and
    /// property values
    /// </summary>
    /// <remarks>
    /// I can't think of a sane way to check indexed
    /// property values
    /// </remarks>
    public void checkPropertyEquality(object expected, object actual) {
      Type expectedType = expected.GetType();
      Type actualType = actual.GetType();
      Assert.AreEqual(expectedType.GetProperties().Length, actualType.GetProperties().Length, "the sets are not equinumerous");
      foreach (PropertyInfo property in expectedType.GetProperties()) {
        if (property.GetIndexParameters().Length == 0) {
          Assert.AreEqual(property.GetValue(expected, null),
              actualType.GetProperty(property.Name).GetValue(actual, null));
        }
        else {
          foreach (PropertyInfo epi in expected.GetType().GetProperties()) {
            ParameterInfo[] paramInfos = epi.GetIndexParameters();
            ArrayList args = new ArrayList();
            foreach (ParameterInfo p in paramInfos) {
              if (p.ParameterType.Equals(typeof(string))) {
                args.Add("hi");
              }
              else if (p.ParameterType.Equals(typeof(int))) {
                //don't use 0 here, because 0 can be interpreted as null
                // and therefore is less likely to expose a bug.
                args.Add(1);
              }
              else if (p.ParameterType.Equals(typeof(object))) {
                args.Add(new Object());
              }
              else {
                Assert.Fail("in general, I don't know how to guess parameter valuse reasonably");
              }
            }
            PropertyInfo api =
              actual.GetType().GetProperty(epi.Name, FPUtils.comprehend("ParameterType", paramInfos));
            object [] input = (object[])args.ToArray(typeof(object));
            Assert.AreEqual(epi.GetValue(expected, input), api.GetValue(actual, input));
          }
        }
      }
    }

    /// <summary>
    /// <see cref="DefensiveDatasource.Chameleon.Emulate" />
    /// should generate classes that have the same
    /// properties as the class of the input.
    /// </summary>
    [Test]
    public void chameleon() {
      foreach (object model in fragileList) {
        object c = DefensiveDatasource.Chameleon.Emulate(model);
        checkPropertyEquality(model, c);
      }
    }

    /// <summary>
    /// We should be able to emulate a given instance
    /// many times. This implies we should be emulate
    /// more than one instance of a given class. When
    /// we call <see cref="DefensiveDatasource.Chameleon.Emulate" />
    /// repeatedly, Chameleon ought to be able to
    /// determine when new classes are (not) needed.
    /// </summary>
    [Test]
    public void RepeatableEmulation() {
      // we ought to be able to emulate the same object more than once
      for (int i = 0; i < 2; ++i) DefensiveDatasource.Chameleon.Emulate(fragileList[0]);

      // let's not generate new classes unnecessarily
      Assert.AreEqual(DefensiveDatasource.Chameleon.Emulate("unicycle").GetType(), DefensiveDatasource.Chameleon.Emulate("bicycle").GetType());
    }

    /// <summary>
    /// The <see cref="DefensiveDatasource.ChameleonEnumerator" /> should
    /// generally obey the IEnumerator contract, and it should wrap
    /// elements in the underlying list with instances of classes
    /// generated by <see cref="DefensiveDatasource.Chameleon.Emulate" />.
    /// </summary>
    [Test]
    public void chameleonEnumerator() {
      DefensiveDatasource.ChameleonEnumerator i = new DefensiveDatasource.ChameleonEnumerator(fragileList);
      try {
        Assert.AreNotEqual(null, i.Current);
        Assert.Fail("A fresh enumerator should be positioned before the first element, with Current as an invalid operation.");
      }
      catch (InvalidOperationException) {
        //expected
      }
      for (int j = 0; j < fragileList.Length; j++) {
        Assert.AreEqual(true, i.MoveNext(), "ChameleonEnumerator doesn't expose as many elements as the underlying list");
      }
      Assert.IsFalse(i.MoveNext(), "ChameleonEnumerator exposes more elements than the underlying list");
      i.Reset();
      IEnumerator k = fragileList.GetEnumerator();
      while (i.MoveNext() && k.MoveNext()) {
        checkPropertyEquality(i.Current, k.Current);
        Assert.AreNotSame(i.Current, k.Current, "the chameleon enumerator isn't wrapping elements");
      }
    }

    /// <summary>
    /// Test <see cref="DefensiveDatasource.Chameleon.Emulate" />'s ability
    /// to generate classes that have only a subset of the properties
    /// in the underlying.
    /// </summary>
    [Test]
    public void filter() {
      Hashtable filter = new Hashtable();
      Hashtable model = new Hashtable();

      string[] selectedProperties = new string[] {"Count", "IsFixedSize"};
      object empty = DefensiveDatasource.Chameleon.Emulate(model, filter);
      foreach (string propName in selectedProperties) {
        filter.Add(propName, 0);
      }
      object length = DefensiveDatasource.Chameleon.Emulate(model, filter);
      object all = DefensiveDatasource.Chameleon.Emulate(model, null);

      Assert.AreEqual(0, empty.GetType().GetProperties().Length, "empty filters should produce objects with no properties");

      foreach (string propName in selectedProperties) {
        object expectedLength = model.GetType().GetProperty(propName).GetValue(model, null);
        object actualLength = length.GetType().GetProperty(propName).GetValue(length, null);
        Assert.AreEqual(
            expectedLength,
            actualLength,
            "non-trivial filters don't work");
      }

      checkPropertyEquality(model, all);
    }

    /// <summary>
    /// Ensure that the original instance underlying
    /// any generated class instance can be recovered
    /// from client code
    /// </summary>
    [Test]
    public void accessibleUnderlying() {
      string underlying = "it's me";
      object c = DefensiveDatasource.Chameleon.Emulate(underlying);
      Assert.AreSame(underlying, DefensiveDatasource.Chameleon.getUnderlying(c));
    }

    public class Super {
      public int ordinaryProperty {
        get {
          return 0;
        }
      }

      public virtual int virtualProperty {
        get {
          return 0;
        }
      }
    }

    private class Sub : Super {
      public override int virtualProperty {
        get {
          return 1;
        }
      }
    }

    //TODO: DefensiveDatasource should? be able to loosen accessibility for private/protected/internal members

    /// <summary>
    /// Some ASP.NET web controls insist that all elements of a collection be assignable to an instance
    /// of the type of the first element in the collection. This is a stronger requirement than is imposed
    /// by other web controls, which merely check for structural similiarity. This test ensures that the
    /// more discerning/nosey web controls can safely cache PropertyInfos obtained from the first element
    /// and use those PIs against the subsequent elements in the enumeration.
    /// </summary>
    [Test]
    public void PropertyInfosWorkForAllResultantElements() {
      foreach (string propertyName in new string[] {"ordinaryProperty", "virtualProperty"}) {
        IEnumerable list = new object[] {new Super(), new Sub()};
        string chameleonTypeName = DefensiveDatasource.NearestCommonAncestorType(list).Name + "Chameleon";
        DefensiveDatasource.ChameleonEnumerator enumerator = new DefensiveDatasource.ChameleonEnumerator(list, null, chameleonTypeName);
        enumerator.MoveNext();
        PropertyInfo pi = enumerator.Current.GetType().GetProperty(propertyName);
        enumerator.MoveNext();
        pi.GetValue(enumerator.Current, null);
      }
    }

    public abstract class Shape {
      public virtual string Name {
        get {
          return "Shape";
        }
      }

      public abstract float Area {
        get;
      }

      private float center;
      public float Center {
        get {
          return center;
        }
        set {
          center = value;
        }
      }
    }

    public class Square : Shape {
      public override string Name {
        get {
          return "Square";
        }
      }

      private float sideLength = 3;
      public float SideLength {
        get {
          return sideLength;
        }
        set {
          sideLength = value;
        }
      }

      public override float Area {
        get {
          return (float)Math.Pow(sideLength, 2);
        }
      }
    }

    public class Circle : Shape {
      public override string Name {
        get {
          return "Circle";
        }
      }

      private float radius = 5;
      public float Radius {
        get { return radius; }
      }

      public override float Area {
        get {
          return (float)(Math.Pow(radius, 2) * Math.PI);
        }
      }
    }

    [Test]
    public void EmptyCollectionsAreBindable() {
      DefensiveDatasource s = new DefensiveDatasource(new ArrayList(), null);
      s.GetEnumerator();
    }

    private class PrivateClass {
      public virtual int Prop {
        get { return 0; }
      }
    }

    [Test]
    public void CompatibleWithPrivateClasses() {
      object privateInstance = new PrivateClass();
      checkPropertyEquality(
        privateInstance,
        DefensiveDatasource.Chameleon.Emulate(privateInstance));
    }

    public sealed class SealedElement {
      public string Text {
        get {
          return "SealedElement" + GetType().Name;
        }
      }
    }
    public class UnsealedElement {
      public string Text {
        get {
          return "UnsealedElement";
        }
      }
    }
    [Test]
    public void SealedFirstElementDoesNotStick() {
      object[] objects = new object[] {new SealedElement(), new UnsealedElement()};
      DefensiveDatasource l = new DefensiveDatasource(objects, null);
      foreach (DictionaryEntry pair in FPUtils.zip(objects, l)) {
        Assert.AreEqual(
          pair.Key,
          DefensiveDatasource.Chameleon.getUnderlying(pair.Value));
        checkPropertyEquality(pair.Key, pair.Value);
      }
    }
    [Test]
    public void UnderlyingDoesNotStick() {
      object[] objects = new object[] {new SealedElement(), new UnsealedElement()};
      object[] chameleons = new object[objects.Length];
      for (int i = 0; i < objects.Length; ++i) {
        chameleons[i] = DefensiveDatasource.Chameleon.Emulate(objects[i]);
        Assert.AreSame(
            objects[i],
            DefensiveDatasource.Chameleon.getUnderlying(chameleons[i]));
      }
    }
    private class UnaryIndexer {
      public int this[int arg] {
        get {
          return arg + 1;
        }
        set {
        }
      }
    }
    private class BinaryIndexer {
      public int this[int argA, string argB] {
        get {
          return 0;
        }
        set {
        }
      }
    }
    private class MultipleIndexers {
      public int this[int arg] {
        get {
          return arg;
        }
        set {
        }
      }
      public int this[int argA, string argB] {
        get {
          return argA + argB.Length;
        }
        set {
        }
      }
      public int this[string argA, string argB] {
        get {
          return argA.Length + argB.Length;
        }
        set {
        }
      }
    }
    private static object _makeDD(object o) {
      return new DefensiveDatasource((IEnumerable)o, null);
    }
    private FPUtils.F makeDD = new FPUtils.F(_makeDD);
    
    [Test]
    public void WorksWithIndexers() {
      //for each arity of interest i, a collection of
      //  objects-with-indexers-of-arity i
      IEnumerable indexerArities = new IEnumerable[] {
        new object [] {new UnaryIndexer()},
        new object [] {new BinaryIndexer()}};
      IEnumerable dataSources = FPUtils.map(makeDD, indexerArities);
      foreach (DictionaryEntry arity in FPUtils.zip(indexerArities, dataSources)) {
        foreach (DictionaryEntry pair in FPUtils.zip((IEnumerable)arity.Key, (IEnumerable)arity.Value)) {
          checkPropertyEquality(pair.Key, pair.Value);
        }
      }
    }
    [Test]
    public void ProxyMultipleIndexers() {
      object [] objects = new object [] {new MultipleIndexers()};
      DefensiveDatasource l = new DefensiveDatasource(objects, null);
      foreach (DictionaryEntry pair in FPUtils.zip(objects, l)) {
        checkPropertyEquality(pair.Key, pair.Value);
      }
    }
  }
}
